package com.hk.commons.poi.excel.read;

import com.hk.commons.poi.excel.exception.ExcelReadException;
import com.hk.commons.poi.excel.metadata.ErrorLog;
import com.hk.commons.poi.excel.metadata.ReadResult;
import com.hk.commons.poi.excel.metadata.ReadWorkbook;
import com.hk.commons.poi.excel.metadata.Title;
import com.hk.commons.poi.excel.read.dom.DomExcelReadExecutor;
import com.hk.commons.poi.excel.read.listeners.ReadListener;
import com.hk.commons.poi.excel.read.sax.v03.SaxXlsExcelReadExecutor;
import com.hk.commons.poi.excel.read.sax.v07.SaxXlsxExcelReadExecutor;
import com.hk.commons.poi.excel.read.validate.JSRRowValidate;
import com.hk.commons.poi.excel.read.validate.RowValidate;
import com.hk.commons.poi.excel.util.ReadExcelUtils;
import com.hk.commons.util.CollectionUtils;
import com.hk.commons.util.JsonUtils;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.poifs.crypt.Decryptor;
import org.apache.poi.poifs.filesystem.DocumentFactoryHelper;
import org.apache.poi.poifs.filesystem.POIFSFileSystem;
import org.apache.poi.util.IOUtils;

import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.util.*;
import java.util.concurrent.ConcurrentLinkedQueue;

/**
 * Excel 读取
 *
 * @param <T>
 * @author Kevin
 */
public final class ExcelReader<T> {

    private final ReadWorkbook readWorkbook;

    private final List<ReadListener<T>> listeners = new ArrayList<>();

    private final List<RowValidate<T>> validates = new ArrayList<>();

    public ExcelReader(ReadWorkbook readWorkbook) {
        this.readWorkbook = readWorkbook;
    }

    private ExcelReadExecutor<T> choiceExcelExecutor() {
        return switch (readWorkbook.getReadMode()) {
            case DOM -> new DomExcelReadExecutor<>(readWorkbook);
            case SAX -> {
                ExcelTypeEnum excelType = ExcelTypeEnum.valueOf(readWorkbook);
                yield switch (excelType) {
                    case XLS -> {
                        try {
                            POIFSFileSystem poifsFileSystem;
                            if (readWorkbook.getFile() != null) {
                                poifsFileSystem = new POIFSFileSystem(readWorkbook.getFile());
                            } else {
                                poifsFileSystem = new POIFSFileSystem(readWorkbook.getInputStream());
                            }
                            // So in encrypted excel, it looks like XLS but it's actually XLSX
                            if (poifsFileSystem.getRoot().hasEntry(Decryptor.DEFAULT_POIFS_ENTRY)) {
                                InputStream decryptedStream = null;
                                try {
                                    decryptedStream = DocumentFactoryHelper
                                            .getDecryptedStream(poifsFileSystem.getRoot().getFileSystem(),
                                                    readWorkbook.getPassword());
                                    yield new SaxXlsxExcelReadExecutor<>(readWorkbook, decryptedStream);
                                } finally {
                                    IOUtils.closeQuietly(decryptedStream);
                                    // as we processed the full stream already, we can close the filesystem here
                                    // otherwise file handles are leaked
                                    poifsFileSystem.close();
                                }
                            }
                            yield new SaxXlsExcelReadExecutor<>(readWorkbook, poifsFileSystem);
                        } catch (IOException e) {
                            throw new ExcelReadException(e.getMessage(), e);
                        }
                    }
                    case XLSX -> new SaxXlsxExcelReadExecutor<>(readWorkbook);
                };
            }
        };
    }

    public ExcelReader<T> readListener(ReadListener<T> readListener) {
        if (Objects.nonNull(readListener)) {
            listeners.add(readListener);
        }
        return this;
    }

    @SafeVarargs
    public final ExcelReader<T> readListeners(ReadListener<T>... readListeners) {
        CollectionUtils.addAllNotNull(this.listeners, readListeners);
        return this;
    }

    public ExcelReader<T> readListenerList(Collection<ReadListener<T>> readListenerList) {
        this.listeners.addAll(readListenerList);
        return this;
    }

    /**
     * use JSR-303 Bean Validation
     *
     * @param groups groups
     */
    public ExcelReader<T> useJSR303Validate(Class<?>... groups) {
        this.validates.add(new JSRRowValidate<>(groups));
        return this;
    }

    public ExcelReader<T> addValidate(RowValidate<T> validate) {
        if (Objects.nonNull(validate)) {
            this.validates.add(validate);
        }
        return this;
    }

    @SafeVarargs
    public final ExcelReader<T> addValidates(RowValidate<T>... rowValidates) {
        CollectionUtils.addAllNotNull(this.validates, rowValidates);
        return this;
    }

    public ExcelReader<T> addValidateList(Collection<RowValidate<T>> rowValidateList) {
        this.validates.addAll(rowValidateList);
        return this;
    }

    /**
     * 开始读取excel
     */
    public ReadResult<T> read() {
        if (null == readWorkbook.getColumnField()) {
            readWorkbook.setColumnField(ReadExcelUtils.getReadExcelAnnotationMapping(readWorkbook.getBeanClass()));
        }
        var excelReadExecutor = choiceExcelExecutor();
        if (CollectionUtils.isNotEmpty(this.validates)) {
            this.listeners.add(new ConcurrentValidateListener<>(this.validates));
        }
        excelReadExecutor.addReadListeners(this.listeners);
        return excelReadExecutor.execute();
    }


    /**
     * 子线程验证监听器，主线程读取每一行时，将读取的行对象放在 队列中，子线程读取队列中的数据
     */
    @Slf4j
    @RequiredArgsConstructor
    private static class ConcurrentValidateListener<T> implements ReadListener<T> {

        private final List<RowValidate<T>> validates;
        /**
         * 验证保存错误数据
         */
        private final List<ErrorLog<T>> errorLogs = new ArrayList<>();
        private Queue<RowInfo> queue;
        /**
         * <pre>
         * 主线程也执行任务时，任务队列中的剩余数量大小
         * 当主线程解析Excel完成后，子线程验证解析数据的有效性，如果队列中的任务大数大于此值，主线程也一起来解析，防止主线程闲置，而子线程任务繁忙的情况
         * </pre>
         */
        @Setter
        private int executionTaskSize = 100;
        /**
         * 主线程读取Excel 的状态
         * 读取完后，会标记为 false(已完成)
         */
        private volatile boolean completeState;
        private Thread thread;

        @Override
        public void onStart() {
            if (CollectionUtils.isNotEmpty(this.validates)) {
                this.queue = new ConcurrentLinkedQueue<>();
                this.completeState = false;
                thread = new Thread(this::executeTask);
                this.thread.start();
            }
        }

        private void executeTask() {
            do {
                while (!queue.isEmpty()) {
                    var rowInfo = queue.poll();
                    log.debug("poll row,sheetIndex:{}, sheetName:{},row:{},rowData: {}", rowInfo.getSheetIndex(),
                            rowInfo.getSheetName(), rowInfo.getRowNumber(), JsonUtils.serialize(rowInfo.getData()));
                    for (var item : this.validates) {
                        var invalidCells = item.validate(rowInfo.getData(),
                                rowInfo.getRowNumber(), rowInfo.getTitleList(), rowInfo.getSheetIndex(),
                                rowInfo.getSheetName());
                        if (CollectionUtils.isNotEmpty(invalidCells)) {
                            this.errorLogs.add(new ErrorLog<>(rowInfo.getSheetIndex(), rowInfo.getSheetName(),
                                    rowInfo.getRowNumber(), rowInfo.getData(), invalidCells));
                            if (!item.errorNext()) {
                                break;
                            }
                        }
                    }
                }
                if (!completeState) { // 当没有标记为 completeState 时，让出时间片给其它线程
                    Thread.yield();
                }
            } while (!completeState);
        }

        @Override
        public void onRowEnd(int sheetIndex, String sheetName, int row, T data, List<Title> titleList) {
            if (Objects.nonNull(queue)) {
                log.debug("offer row,sheetIndex:{}, sheetName:{},row:{},rowData: {}", sheetIndex, sheetName, row,
                        JsonUtils.serialize(data));
                queue.offer(new RowInfo(data, row, titleList, sheetIndex, sheetName));
            }
        }

        @Override
        public List<ErrorLog<T>> onCompleted() {
            this.completeState = true; // 标记为完成，使用主线程和thread 同时执行剩余存留在队列中任务，防止任务过多时，主线程闲置，而 thread 在执行
            if (this.queue.size() > executionTaskSize) {
                executeTask(); // 主线程也一起执行队列中的任务。
            }
            if (null != thread) { // 等待 thread 也执行完成
                try {
                    thread.join();
                } catch (InterruptedException e) {
                    log.error(e.getMessage(), e);
                }
            }
            return this.errorLogs;
        }

        @Data
        @AllArgsConstructor
        private class RowInfo implements Serializable {

            private T data;

            private int rowNumber;

            private List<Title> titleList;

            private int sheetIndex;

            private String sheetName;
        }
    }

}
